package com.gintama.common.filter;

import cn.hutool.core.util.CharsetUtil;
import cn.hutool.core.util.ReUtil;
import cn.hutool.core.util.StrUtil;
import com.gintama.base.constants.BaseConstants;
import com.gintama.base.util.JacksonUtils;
import com.gintama.common.annotation.TraceLogLimit;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.MDC;
import org.springframework.http.HttpStatus;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.util.ContentCachingRequestWrapper;
import org.springframework.web.util.ContentCachingResponseWrapper;
import org.springframework.web.util.WebUtils;

/**
 * HTTP请求跟踪日志过滤器
 * @author hao.hu
 * @since 2020年4月22日
 *
 */
@Slf4j
public class HttpTraceLogFilter extends OncePerRequestFilter {

    /**
     * 忽略前缀.
     */
    private static final String[] IGNORE_TRACE_PATH_PREFIX = new String[]{};
    /**
     * 忽略内容.
     */
    private static final String[] IGNORE_TRACE_PATH_CONTENT = new String[]{
            "/v2/api-docs", "/webjars/springfox-swagger-ui", "/druid/**.json", "/druid/**.html",
            "/swagger-resources", "/health", "/healthy", "/ready"};
    /**
     * 忽略后缀.
     */
    private static final String[] IGNORE_TRACE_PATH_SUFFIX = new String[]{".js",
            ".css", ".ico", ".jpg", ".png"};

    /**
     * 忽略出参 content_type
     */
    private static final String[] IGNORE_RSPONSE_CONTENT_TYPE = new String[]{"application/vnd.ms-excel","application/zip"};

    /**
     * 忽略入参 content_type
     */
    private static final String[] IGNORE_REQUEST_CONTENT_TYPE = new String[]{"multipart/form-data"};

    /**
     * 实例化新的Http跟踪日志筛选器.
     */
    public HttpTraceLogFilter() {
    }

    /**
     * 过滤处理日志 - 与doFilter相同的合同，但保证在单个请求线程中每个请求只调用一次。
     *
     * @param request     请求
     * @param response    响应
     * @param filterChain 过滤链
     * @throws ServletException Servlet异常
     * @throws IOException      IO异常
     */
    @Override
    protected void doFilterInternal(HttpServletRequest request,
            HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        if (!isRequestValid(request)) {
            filterChain.doFilter(request, response);
            return;
        }
        if (!(request instanceof ContentCachingRequestWrapper)) {
            request = new ContentCachingRequestWrapper(request);
        }
        if (!(response instanceof ContentCachingResponseWrapper)) {
            response = new ContentCachingResponseWrapper(response);
        }
        int status = HttpStatus.INTERNAL_SERVER_ERROR.value();
        long startTime = System.currentTimeMillis();
        try {
            // 记录日志Id
            String ptxId = request.getHeader(BaseConstants.PTX_ID);
            // 没有则说明是链路开端，初始化日志Id
            if (StrUtil.isBlank(ptxId)) {
                ptxId = UUID.randomUUID().toString();
            }
            MDC.put(BaseConstants.PTX_ID, ptxId);

            filterChain.doFilter(request, response);
            status = response.getStatus();
        } finally {
            String path = request.getRequestURI();
            if (this.needRecord(path)) {
                // 1. 记录日志
                HttpTraceLog traceLog = new HttpTraceLog();
                traceLog.setPath(path);
                traceLog.setMethod(request.getMethod());
                long latency = System.currentTimeMillis() - startTime;
                traceLog.setTimeTaken(latency);
                traceLog.setTime(LocalDateTime.now().toString());
                traceLog.setParameterMap(
                        JacksonUtils.toJsonString(request.getParameterMap()));
                traceLog.setStatus(status);
                traceLog.setRequestBody(getRequestBody(request));
                traceLog.setResponseBody(getResponseBody(response));
                log.info("Http trace log: {}",
                        JacksonUtils.toJsonString(traceLog));
            }
            updateResponse(response);
        }
    }

    private boolean isRequestValid(HttpServletRequest request) {
        try {
            new URI(request.getRequestURL().toString());
            return true;
        } catch (URISyntaxException ex) {
            return false;
        }
    }

    /**
     * 是否需要记录日志.
     *
     * @param uri
     * @return
     */
    private boolean needRecord(String uri) {
        if (StringUtils.isBlank(uri)) {
            return false;
        }
        List<String> prefix = Arrays.stream(IGNORE_TRACE_PATH_PREFIX)
                .filter(uri::startsWith).collect(Collectors.toList());
        Set<String> content = new HashSet<>(IGNORE_TRACE_PATH_CONTENT.length);
        for (String pathContent : IGNORE_TRACE_PATH_CONTENT) {
            if (StrUtil.contains(uri, pathContent)) {
                content.add(pathContent);
            } else {
                String regExp = StrUtil.replace(pathContent, "*", "$*");
                regExp = StrUtil.replace(regExp, "$*$*", "(/?[\\w-]+)*");
                regExp = StrUtil.replace(regExp, "$*", "[\\w-]+");
                if (ReUtil.isMatch(regExp, uri)) {
                    content.add(pathContent);
                }
            }
        }
        List<String> suffix = Arrays.stream(IGNORE_TRACE_PATH_SUFFIX)
                .filter(uri::endsWith).collect(Collectors.toList());
        return prefix.isEmpty() && content.isEmpty() && suffix.isEmpty();
    }

    /**
     * 获取请求内容.
     *
     * @param request
     * @return
     */
    private String getRequestBody(HttpServletRequest request) {
        String requestBody = "";
        ContentCachingRequestWrapper wrapper = WebUtils
                .getNativeRequest(request, ContentCachingRequestWrapper.class);
        if (wrapper != null) {
            try {
                if (needIgnoreRequestContentType(request.getContentType())) {
                    requestBody = "<MultipartHttpServletRequest>";
                } else {
                    requestBody = new String(wrapper.getContentAsByteArray(),
                            wrapper.getCharacterEncoding());
                }
            } catch (IOException e) {
                // NOOP
            }
        }
        return requestBody;
    }

    /**
     * 获取响应内容.
     *
     * @param response
     * @return
     */
    private String getResponseBody(HttpServletResponse response) {
        String responseBody = "";
        ContentCachingResponseWrapper wrapper = WebUtils.getNativeResponse(
                response, ContentCachingResponseWrapper.class);
        if (wrapper != null) {
            try {
                if (needIgnoreResponseContentType(response.getContentType())) {
                    responseBody = "<ContentType="
                            + response.getContentType() + ">";
                } else {
                    String maxLengthStr = response.getHeader(TraceLogLimit.RESPONSE_HEAD_TRACE_LOG_LIMIT);
                    // 默认使用UTF-8编码
                    String characterEncoding =
                            CharsetUtil.ISO_8859_1.equalsIgnoreCase(wrapper.getCharacterEncoding()) ? CharsetUtil.UTF_8
                                    : wrapper.getCharacterEncoding();
                    if (StringUtils.isEmpty(maxLengthStr)) {
                        responseBody = new String(wrapper.getContentAsByteArray(),
                                characterEncoding);
                    } else {
                        int maxLength = Integer.valueOf(maxLengthStr);
                        responseBody = new String(wrapper.getContentAsByteArray(),
                                characterEncoding);
                        if (responseBody.length() > maxLength) {
                            responseBody = responseBody.substring(0, maxLength) + TraceLogLimit.EXCEEDED_LENGTH_NOTICE;
                        }
                    }
                }
            } catch (IOException e) {
                // NOOP
            }
        }
        return responseBody;
    }

    /**
     * 判断是否是不用记录返回值类型的contentType
     * @param contentType:
     * @return Boolean
     * @author SunFei
     * @since 2022/6/13 18:44
     */
    private Boolean needIgnoreResponseContentType(String contentType){
        Boolean needIgnore = Boolean.FALSE;
        if(StringUtils.isEmpty(contentType)){
            return needIgnore;
        }
        for(String info:IGNORE_RSPONSE_CONTENT_TYPE){
            if(contentType.toLowerCase().contains(info)){
                needIgnore = Boolean.TRUE;
                break;
            }
        }
        return needIgnore;
    }

    /**
     * @param contentType:
     * @return Boolean
     * @author SunFei
     * @description TODO 判断是否是不用记录请求值类型的contentType
     * @since 2022/6/13 18:44
     */
    private Boolean needIgnoreRequestContentType(String contentType){
        Boolean needIgnore = Boolean.FALSE;
        if(StringUtils.isEmpty(contentType)){
            return needIgnore;
        }
        for(String info:IGNORE_REQUEST_CONTENT_TYPE){
            if(contentType.toLowerCase().contains(info)){
                needIgnore = Boolean.TRUE;
                break;
            }
        }
        return needIgnore;
    }


    /**
     * 更新响应内容.
     *
     * @param response
     * @throws IOException
     */
    private void updateResponse(HttpServletResponse response)
            throws IOException {
        ContentCachingResponseWrapper responseWrapper = WebUtils
                .getNativeResponse(response,
                        ContentCachingResponseWrapper.class);
        Objects.requireNonNull(responseWrapper).copyBodyToResponse();
    }

    @Data
    private static class HttpTraceLog {

        private String path;
        private String parameterMap;
        private String method;
        private Long timeTaken;
        private String time;
        private Integer status;
        private String requestBody;
        private String responseBody;
    }

}
